/*
 * Copyright (c) 2014, 2019, Oracle and/or its affiliates. All rights reserved.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License, version 2.0,
 * as published by the Free Software Foundation.
 *
 * This program is also distributed with certain software (including
 * but not limited to OpenSSL) that is licensed under separate terms, as
 * designated in a particular file or component or in included license
 * documentation.  The authors of MySQL hereby grant you an additional
 * permission to link the program and your derivative works with the
 * separately licensed software that they have included with MySQL.
 * This program is distributed in the hope that it will be useful,  but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See
 * the GNU General Public License, version 2.0, for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
 */

#include "mforms/home_screen_connections.h"

#include "mforms/menu.h"
#include "mforms/popup.h"
#include "mforms/imagebox.h"
#include "mforms/scrollpanel.h"
#include "base/string_utilities.h"
#include "base/file_utilities.h"
#include "base/log.h"
#include "base/any.h"

DEFAULT_LOG_DOMAIN("home");

using namespace base;
using namespace mforms;

//----------------- ConnectionsSection ---------------------------------------------------------------------------------

class mforms::ConnectionEntry : public base::Accessible {
  friend class ConnectionsSection;

public:
  std::string connectionId;

protected:
  ConnectionsSection *owner;

  std::string title;
  std::string description;
  std::string user;
  std::string schema;
  bool compute_strings; // True after creation to indicate the need to compute the final display strings.
  bool draw_info_tab;

  // For filtering we need the full strings.
  std::string search_title;
  std::string search_description;
  std::string search_user;
  std::string search_schema;

  base::Rect bounds;

  //------ Accessibility Methods ---------------------------------------------------------------------------------------

  virtual std::string getAccessibilityDescription() override {
    return title;
  }

  //--------------------------------------------------------------------------------------------------------------------

  virtual std::string getAccessibilityTitle() override {
    return title;
  }

  //--------------------------------------------------------------------------------------------------------------------

  virtual std::string getAccessibilityValue() override {
    std::string result = "host: " + description;
    if (!schema.empty())
      result += ", schema: " + schema;
    if (!user.empty())
      result += ", user: " + user;
    return result;
  }

  //--------------------------------------------------------------------------------------------------------------------

  virtual Accessible::Role getAccessibilityRole() override {
    return Accessible::PushButton;
  }

  //--------------------------------------------------------------------------------------------------------------------

  virtual base::Rect getAccessibilityBounds() override {
    return bounds;
  }

  //--------------------------------------------------------------------------------------------------------------------

  virtual void accessibilityDoDefaultAction() override {
    activate();
  };

  //--------------------------------------------------------------------------------------------------------------------

  virtual std::string getAccessibilityDefaultAction() override {
    return "click";
  }

  //--------------------------------------------------------------------------------------------------------------------

  virtual bool accessibilityGrabFocus() override {
    owner->setFocusOnEntry(this);
    return true;
  }

  //--------------------------------------------------------------------------------------------------------------------

  virtual void accessibilityShowMenu() override {
    if (owner->_connection_context_menu != nullptr) {
      owner->_connection_context_menu->popup_at(owner, static_cast<int>(bounds.xcenter()),
        static_cast<int>(bounds.ycenter()));
    }
  };

  //--------------------------------------------------------------------------------------------------------------------

  /**
   * Draws the icon followed by the given text. The given position is that of the upper left corner
   * of the image.
   */
  void draw_icon_with_text(cairo_t *cr, double x, double y, cairo_surface_t *icon, const std::string &text,
                           double alpha) {
    if (icon) {
      mforms::Utilities::paint_icon(cr, icon, x, y);
      x += imageWidth(icon) + 3;
    }

    base::Color titleColor = getTitleColor();
    cairo_set_source_rgba(cr, titleColor.red, titleColor.green, titleColor.blue, titleColor.alpha);

    std::vector<std::string> texts = base::split(text, "\n");

    for (size_t index = 0; index < texts.size(); index++) {
      cairo_text_extents_t extents;
      std::string line = texts[index];
      cairo_text_extents(cr, line.c_str(), &extents);

      cairo_move_to(cr, x, (int)(y + imageHeight(icon) / 2.0 + extents.height / 2.0 + (index * (extents.height + 3))));
      cairo_show_text(cr, line.c_str());
      cairo_stroke(cr);
    }
  }

  //--------------------------------------------------------------------------------------------------------------------

public:
  enum ItemPosition { First, Last, Other };

  //--------------------------------------------------------------------------------------------------------------------

  ConnectionEntry(ConnectionsSection *aowner) : owner(aowner), compute_strings(false) {
    draw_info_tab = true;
  }

  //--------------------------------------------------------------------------------------------------------------------

  virtual bool is_movable() const {
    return true;
  }

  //--------------------------------------------------------------------------------------------------------------------

  virtual base::Color getTitleColor() const {
    return owner->_titleColor;
  }

  //--------------------------------------------------------------------------------------------------------------------

  virtual base::Color getBackgroundColor(bool hot) const {
    return hot ? owner->_backgroundColorHot : owner->_backgroundColor;
  }

  //--------------------------------------------------------------------------------------------------------------------

  virtual cairo_surface_t *get_background_icon() const {
    return owner->_sakila_icon;
  }

  //--------------------------------------------------------------------------------------------------------------------

  void draw_tile_background(cairo_t *cr, bool hot, double alpha, bool for_dragging) const {
    base::Color backColor = getBackgroundColor(hot);

    base::Rect bounds = this->bounds;
    if (for_dragging)
      bounds.pos = base::Point(0, 0);

    bounds.use_inter_pixel = false;
    cairo_rectangle(cr, bounds.left(), bounds.top(), bounds.width(), bounds.height());
    cairo_set_source_rgba(cr, backColor.red, backColor.green, backColor.blue, alpha);
    cairo_fill(cr);

    // Border.
    bounds.use_inter_pixel = true;
    cairo_rectangle(cr, bounds.left(), bounds.top(), bounds.width() - 1, bounds.height() - 1);

    if (owner->_owner->isDarkModeActive())
      cairo_set_source_rgba(cr, backColor.red + 0.1, backColor.green + 0.1, backColor.blue + 0.1, alpha);
    else
      cairo_set_source_rgba(cr, backColor.red - 0.05, backColor.green - 0.05, backColor.blue - 0.05, alpha);

    cairo_set_line_width(cr, 1);
    cairo_stroke(cr);

    float image_alpha = 0.25;

    // Background icon.
    bounds.use_inter_pixel = false;
    cairo_surface_t *back_icon = get_background_icon();

    double x = bounds.left() + bounds.width() - imageWidth(back_icon);
    double y = bounds.top() + bounds.height() - imageHeight(back_icon);
    cairo_set_source_surface(cr, back_icon, x, y);
    cairo_paint_with_alpha(cr, image_alpha * alpha);
  }

  //--------------------------------------------------------------------------------------------------------------------

  virtual void draw_tile(cairo_t *cr, bool hot, double alpha, bool for_dragging) {
    base::Color titleColor = getTitleColor();
    base::Rect bounds = this->bounds;
    if (for_dragging)
      bounds.pos = base::Point(0, 0);

    draw_tile_background(cr, hot, alpha, for_dragging);

    cairo_set_source_rgba(cr, titleColor.red, titleColor.green, titleColor.blue, alpha);

    std::string systemFont = base::OSConstants::defaultFontName();
    cairo_select_font_face(cr, systemFont.c_str(), CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_NORMAL);
    cairo_set_font_size(cr, mforms::HomeScreenSettings::HOME_TILES_TITLE_FONT_SIZE);

    // Title string.
    double x = (int)bounds.left() + 10.5; // Left offset from the border to caption, user and network icon.
    double y = bounds.top() + 27;         // Distance from top to the caption base line.

    if (compute_strings) {
      // On first render compute the actual string to show. We only need to do this once
      // as neither the available space changes nor is the entry manipulated.

      // We try to shrink titles in the middle. If there's a colon in it we assume it's a port number
      // and thus, we shrink everything before the colon.
      if (title.find(':') != std::string::npos) {
        double available_width = bounds.width() - 21;
        std::string left, right;
        cairo_text_extents_t extents;
        base::partition(title, ":", left, right);
        right = ":" + right;
        cairo_text_extents(cr, right.c_str(), &extents);
        available_width -= extents.width;
        title = mforms::Utilities::shorten_string(cr, left, available_width) + right;
      } else {
        double available_width = bounds.width() - 21;
        title = mforms::Utilities::shorten_string(cr, title, available_width);
      }
    }

    cairo_move_to(cr, x, y);
    cairo_show_text(cr, title.c_str());
    cairo_stroke(cr);

    cairo_set_font_size(cr, mforms::HomeScreenSettings::HOME_SMALL_INFO_FONT_SIZE);

    draw_tile_text(cr, x, y, alpha);

    compute_strings = false;
  }

  //--------------------------------------------------------------------------------------------------------------------

  virtual void draw_tile_text(cairo_t *cr, double x, double y, double alpha) {
    if (compute_strings) {
      double available_width = bounds.width() - 24 - imageWidth(owner->_network_icon);
      description = mforms::Utilities::shorten_string(cr, description, available_width);

      available_width =
        bounds.center().x - x - imageWidth(owner->_user_icon) - 6; // -6 is the spacing between text and icons.
      user = mforms::Utilities::shorten_string(cr, user, available_width);

      schema = mforms::Utilities::shorten_string(cr, schema, available_width);
    }

    y = bounds.top() + 56 - imageHeight(owner->_user_icon);
    draw_icon_with_text(cr, x, y, owner->_user_icon, user, alpha);

    y = bounds.top() + 74 - imageHeight(owner->_network_icon);
    draw_icon_with_text(cr, x, y, owner->_network_icon, description, alpha);
  }

  //--------------------------------------------------------------------------------------------------------------------

  virtual void activate() {
    owner->_owner->trigger_callback(HomeScreenAction::ActionOpenConnectionFromList, connectionId);
  }

  //--------------------------------------------------------------------------------------------------------------------

  virtual mforms::Menu *context_menu() const {
    return owner->_connection_context_menu;
  }

  //--------------------------------------------------------------------------------------------------------------------

  virtual void menu_open(ItemPosition pos) const {
    mforms::Menu *menu = context_menu();

    menu->set_item_enabled(menu->get_item_index("edit_connection"), true);
    menu->set_item_enabled(menu->get_item_index("move_connection_to_group"), true);
    menu->set_item_enabled(menu->get_item_index("delete_connection"), true);
    menu->set_item_enabled(menu->get_item_index("delete_connection_all"), true);

    menu->set_item_enabled(menu->get_item_index("move_connection_to_top"), pos != First);
    menu->set_item_enabled(menu->get_item_index("move_connection_up"), pos != First);
    menu->set_item_enabled(menu->get_item_index("move_connection_down"), pos != Last);
    menu->set_item_enabled(menu->get_item_index("move_connection_to_end"), pos != Last);
  }

  //--------------------------------------------------------------------------------------------------------------------

};

//----------------------------------------------------------------------------------------------------------------------

class mforms::FolderEntry : public ConnectionEntry, public std::enable_shared_from_this<mforms::FolderEntry> {
protected:

  //--------------------------------------------------------------------------------------------------------------------

  virtual std::string getAccessibilityDescription() override {
    return "Connection Group";
  }

  virtual std::string getAccessibilityTitle() override {
    return title;
  }

  //--------------------------------------------------------------------------------------------------------------------

  virtual void accessibilityShowMenu() override {
    if (owner->_folder_context_menu != nullptr) {
      owner->_folder_context_menu->popup_at(owner, static_cast<int>(bounds.xcenter()),
        static_cast<int>(bounds.ycenter()));
    }
  };

  //--------------------------------------------------------------------------------------------------------------------

public:
  std::vector<std::shared_ptr<ConnectionEntry> > children;

  //--------------------------------------------------------------------------------------------------------------------

  FolderEntry(ConnectionsSection *aowner) : ConnectionEntry(aowner) {
    draw_info_tab = false;
  }

  //--------------------------------------------------------------------------------------------------------------------

  virtual void draw_tile_text(cairo_t *cr, double x, double y, double alpha) override {
    base::Color titleColor = getTitleColor();
    cairo_set_source_rgba(cr, titleColor.red, titleColor.green, titleColor.blue, titleColor.alpha);

    std::string info = std::to_string(children.size() - 1) + " " + _("Connections");
    y = bounds.top() + 55;
    cairo_move_to(cr, x, y);
    cairo_show_text(cr, info.c_str());
    cairo_stroke(cr);
  }

  //--------------------------------------------------------------------------------------------------------------------

  virtual mforms::Menu *context_menu() const override {
    return owner->_folder_context_menu;
  }

  //--------------------------------------------------------------------------------------------------------------------

  virtual void menu_open(ItemPosition pos) const override {
    mforms::Menu *menu = context_menu();

    menu->set_item_enabled(menu->get_item_index("move_connection_to_top"), pos != First);
    menu->set_item_enabled(menu->get_item_index("move_connection_up"), pos != First);
    menu->set_item_enabled(menu->get_item_index("move_connection_down"), pos != Last);
    menu->set_item_enabled(menu->get_item_index("move_connection_to_end"), pos != Last);
  }

  //--------------------------------------------------------------------------------------------------------------------

  virtual void activate() override {
    owner->change_to_folder(shared_from_this());
  }

  //--------------------------------------------------------------------------------------------------------------------

  virtual base::Color getTitleColor() const override {
    return owner->_folderTitleColor;
  }

  //--------------------------------------------------------------------------------------------------------------------

  virtual base::Color getBackgroundColor(bool hot) const override {
    return hot ? owner->_folderBackgroundColorHot : owner->_folderBackgroundColor;
  }

  //--------------------------------------------------------------------------------------------------------------------

  virtual cairo_surface_t *get_background_icon() const override {
    return owner->_folder_icon;
  }

  //--------------------------------------------------------------------------------------------------------------------

};

class mforms::FolderBackEntry : public ConnectionEntry {
protected:

  //--------------------------------------------------------------------------------------------------------------------

  virtual void accessibilityShowMenu() override {
  };

  //--------------------------------------------------------------------------------------------------------------------

  virtual std::string getAccessibilityDescription() override {
      return "Back";
  }

  //--------------------------------------------------------------------------------------------------------------------

public:

  FolderBackEntry(ConnectionsSection *aowner) : ConnectionEntry(aowner) {
    title = "< back";
  }

  //--------------------------------------------------------------------------------------------------------------------

  virtual bool is_movable() const override {
    return false;
  }

  //--------------------------------------------------------------------------------------------------------------------

  virtual base::Color getTitleColor() const override {
    return owner->_folderTitleColor;
  }

  //--------------------------------------------------------------------------------------------------------------------

  virtual base::Color getBackgroundColor(bool hot) const override {
    return hot ? owner->_backTileBackgroundColorHot : owner->_backTileBackgroundColor;
  }

  //--------------------------------------------------------------------------------------------------------------------

  virtual cairo_surface_t *get_background_icon() const override {
    return owner->_folder_icon;
  }

  //--------------------------------------------------------------------------------------------------------------------

  /**
   * Separate tile drawing for the special back tile (to return from a folder).
   */
  virtual void draw_tile(cairo_t *cr, bool hot, double alpha, bool for_dragging) override {
    draw_tile_background(cr, hot, alpha, for_dragging);

    // Title string.
    double x = bounds.left() + 10;
    double y = bounds.top() + 27;
    cairo_set_font_size(cr, mforms::HomeScreenSettings::HOME_TILES_TITLE_FONT_SIZE);
    base::Color titleColor = getTitleColor();
    cairo_set_source_rgba(cr, titleColor.red, titleColor.green, titleColor.blue, titleColor.alpha);

    cairo_move_to(cr, x, y);
    cairo_show_text(cr, _("< back"));
    cairo_stroke(cr);
  }

  //--------------------------------------------------------------------------------------------------------------------

  virtual mforms::Menu *context_menu() const override {
    return nullptr;
  }

  //--------------------------------------------------------------------------------------------------------------------

  virtual void menu_open(ItemPosition pos) const override {
  }

  //--------------------------------------------------------------------------------------------------------------------

  virtual void activate() override {
    owner->change_to_folder(std::shared_ptr<FolderEntry>());
  }

  //--------------------------------------------------------------------------------------------------------------------

};

//----------------- ConnectionsWelcomeScreen ---------------------------------------------------------------------------

ConnectionsWelcomeScreen::ConnectionsWelcomeScreen(HomeScreen *owner) : _owner(owner) {
  logDebug("Creating Connections Welcome Screen\n");

  _closeHomeScreenButton.title = "Close Welcome Message Screen";
  _closeHomeScreenButton.description = "Close Welcome Message Screen";
  _closeHomeScreenButton.defaultHandler = [this]() {
    _owner->trigger_callback(HomeScreenAction::CloseWelcomeMessage, base::any());
  };

  _browseDocButton.title = "浏览文档 >";
  _browseDocButton.description = "浏览文档";
  _browseDocButton.defaultHandler =  [this]() {
    _owner->trigger_callback(HomeScreenAction::ActionOpenDocs, base::any());
  };

  _readBlogButton.title = "阅读博客 >";
  _readBlogButton.description = "打开博客";
  _readBlogButton.defaultHandler =  [this]() {
    _owner->trigger_callback(HomeScreenAction::ActionOpenBlog, base::any());
  };

  _discussButton.title = "论坛讨论 >";
  _discussButton.description = "打开论坛";
  _discussButton.defaultHandler =  [this]() {
    _owner->trigger_callback(HomeScreenAction::ActionOpenForum, base::any());
  };

  _closeIcon = nullptr;

  _heading = "欢迎使用 MySQL Workbench";
  _content = {
    "MySQL Workbench 是 MySQL 官方图形用户界面(GUI)工具。",
    "它允许您设计、创建和浏览数据库模式，处理数据库对象和插入数据，以及设计和运行SQL查询来处理存储的数据。",
    "您还可以将模式和数据从其他数据库供应商迁移到您的MySQL数据库。"
  };
}

//----------------------------------------------------------------------------------------------------------------------

ConnectionsWelcomeScreen::~ConnectionsWelcomeScreen() {
  deleteSurface(_closeIcon);
}

//----------------------------------------------------------------------------------------------------------------------

base::Size ConnectionsWelcomeScreen::getLayoutSize(base::Size proposedSize) {
  return base::Size(proposedSize.width, _totalHeight); // Height doesn't change. Constant content.
}

//----------------------------------------------------------------------------------------------------------------------

Accessible::Role ConnectionsWelcomeScreen::getAccessibilityRole() {
  return base::Accessible::Pane;
}

//----------------------------------------------------------------------------------------------------------------------

std::string ConnectionsWelcomeScreen::getAccessibilityTitle() {
  return _heading;
}

//----------------------------------------------------------------------------------------------------------------------

std::string ConnectionsWelcomeScreen::getAccessibilityDescription() {
  return "Home Screen Welcome Page";
}

//----------------------------------------------------------------------------------------------------------------------

std::string ConnectionsWelcomeScreen::getAccessibilityValue() {
  std::string result;
  for (auto &line : _content)
    result += line + "\n";
  return result;
}

//----------------------------------------------------------------------------------------------------------------------

size_t ConnectionsWelcomeScreen::getAccessibilityChildCount() {
  return 4;
}

//----------------------------------------------------------------------------------------------------------------------

Accessible* ConnectionsWelcomeScreen::getAccessibilityChild(size_t index) {
  switch (index) {
    case 1:
      return &_browseDocButton;
    case 2:
      return &_readBlogButton;
    case 3:
      return &_discussButton;
    default:
      return &_closeHomeScreenButton;
  }
}

//----------------------------------------------------------------------------------------------------------------------

base::Rect ConnectionsWelcomeScreen::getAccessibilityBounds() {
  return base::Rect(0, 100, 500, 700);
}

//----------------------------------------------------------------------------------------------------------------------

Accessible* ConnectionsWelcomeScreen::accessibilityHitTest(ssize_t x, ssize_t y) {
  if (_browseDocButton.bounds.contains(static_cast<double>(x), static_cast<double>(y))) {
    return &_browseDocButton;
  }

  if (_discussButton.bounds.contains(static_cast<double>(x), static_cast<double>(y))) {
    return &_discussButton;
  }

  if (_readBlogButton.bounds.contains(static_cast<double>(x), static_cast<double>(y))) {
    return &_readBlogButton;
  }

  if (_closeHomeScreenButton.bounds.contains(static_cast<double>(x), static_cast<double>(y))) {
    return &_closeHomeScreenButton;
  }

  return nullptr;
}

//----------------------------------------------------------------------------------------------------------------------

void ConnectionsWelcomeScreen::repaint(cairo_t *cr, int areax, int areay, int areaw, int areah) {
  Size size = Utilities::getImageSize(_closeIcon);
  _closeHomeScreenButton.bounds = base::Rect(get_width() - size.width - 8, 8, size.width, size.height);

  cairo_save(cr);
  mforms::Utilities::paint_icon(cr, _closeIcon, _closeHomeScreenButton.bounds.left(), _closeHomeScreenButton.bounds.top());

  int yoffset = 100;

  cairo_select_font_face(cr, mforms::HomeScreenSettings::HOME_TITLE_FONT, CAIRO_FONT_SLANT_NORMAL,
                         CAIRO_FONT_WEIGHT_NORMAL);
  cairo_set_font_size(cr, mforms::HomeScreenSettings::HOME_TITLE_FONT_SIZE * 3);
  cairo_set_source_rgb(cr, _textColor.red, _textColor.green, _textColor.blue);

  cairo_text_extents_t extents;
  cairo_text_extents(cr, _heading.c_str(), &extents);

  double x;
  x = get_width() / 2 - (extents.width / 2 + extents.x_bearing);
  cairo_move_to(cr, x, yoffset);
  cairo_show_text(cr, _heading.c_str());
  yoffset += mforms::HomeScreenSettings::HOME_TITLE_FONT_SIZE * 3;

  for (auto &line : _content) {
    cairo_set_font_size(cr, mforms::HomeScreenSettings::HOME_TITLE_FONT_SIZE * 0.8);
    cairo_text_extents(cr, line.c_str(), &extents);
    x = get_width() / 2 - (extents.width / 2 + extents.x_bearing);
    cairo_move_to(cr, x, yoffset);
    cairo_show_text(cr, line.c_str());
    yoffset += (int)extents.height + 10;
  }

  yoffset += 40;

  // Draw heading links
  cairo_select_font_face(cr, mforms::HomeScreenSettings::HOME_TITLE_FONT, CAIRO_FONT_SLANT_NORMAL,
                         CAIRO_FONT_WEIGHT_NORMAL);
  cairo_set_font_size(cr, mforms::HomeScreenSettings::HOME_TITLE_FONT_SIZE * 0.8);

  cairo_set_source_rgb(cr, 0x1b / 255.0, 0xad / 255.0, 0xe8 / 255.0);
  double pos = 0.25;
  for (auto btn : { &_browseDocButton, &_readBlogButton, &_discussButton }) {
    cairo_text_extents(cr, btn->title.c_str(), &extents);
    x = get_width() * pos - (extents.width / 2 + extents.x_bearing);
    cairo_move_to(cr, floor(x), floor(yoffset));
    cairo_show_text(cr, btn->title.c_str());
    btn->bounds = base::Rect(ceil(x), floor(yoffset + extents.y_bearing), ceil(extents.width), ceil(extents.height));
    pos += 0.25;
  }

  _totalHeight = yoffset + 20;

  cairo_restore(cr);
}

//----------------------------------------------------------------------------------------------------------------------

void ConnectionsWelcomeScreen::updateColors() {
  if (_owner->isDarkModeActive()) {
    _textColor = base::Color::parse("#F4F4F4");
  } else {
    _textColor = base::Color::parse("#505050");
  }
}

//----------------------------------------------------------------------------------------------------------------------

void ConnectionsWelcomeScreen::updateIcons() {
  cairo_surface_destroy(_closeIcon);
  if (_owner->isDarkModeActive())
    _closeIcon = Utilities::load_icon("home_screen_close_dark.png", true);
  else
    _closeIcon = Utilities::load_icon("home_screen_close_light.png", true);
}

//----------------------------------------------------------------------------------------------------------------------

bool ConnectionsWelcomeScreen::mouse_click(mforms::MouseButton button, int x, int y) {
  if (button == MouseButtonLeft) {
    HomeAccessibleButton * button = dynamic_cast<HomeAccessibleButton *>(accessibilityHitTest(x, y));
    if (button != nullptr) {
      button->accessibilityDoDefaultAction();
      return true;
    }
  }
  return false;
}

//------------------ ConnectionsSection --------------------------------------------------------------------------------

ConnectionsSection::ConnectionsSection(HomeScreen *owner) : HomeScreenSection("sidebar_wb.png"),
  _search_box(true), _search_text(mforms::SmallSearchEntry), _showWelcomeHeading(true) {

  _owner = owner;
  _welcomeScreen = nullptr;
  _container = nullptr;
  _connection_context_menu = nullptr;
  _folder_context_menu = nullptr;
  _generic_context_menu = nullptr;
  _drag_index = -1;
  _drop_index = -1;
  _filtered = false;

  _folder_icon = nullptr;
  _network_icon = nullptr;
  _plus_icon = nullptr;
  _sakila_icon = nullptr;
  _user_icon = nullptr;
  _manage_icon = nullptr;

  std::vector<std::string> formats;
  formats.push_back(mforms::HomeScreenSettings::TILE_DRAG_FORMAT); // We allow dragging tiles to reorder them.
  formats.push_back(mforms::DragFormatFileName);                   // We accept sql script files to open them.
  register_drop_formats(this, formats);

  _search_box.set_name("Connection Search Box");

  _search_box.set_spacing(5);
  _search_text.set_size(150, -1);

#ifdef _MSC_VER
  _search_text.set_bordered(false);
  _search_text.set_size(-1, 18);
  _search_text.set_font(mforms::HomeScreenSettings::HOME_NORMAL_FONT);
  _search_box.set_size(-1, 18);
#else
  _search_box.set_padding(8, 1, 8, 5);
  _search_box.set_size(160, 25);
#endif

#ifdef _MSC_VER
  mforms::ImageBox *image = mforms::manage(new mforms::ImageBox, false);
  image->set_image("search_sidebar.png");
  image->set_image_align(mforms::MiddleCenter);
  _search_box.add(image, false, false);
#endif
  _search_text.set_name("搜索文本");
  _search_text.set_placeholder_text("过滤连接");
  _search_text.set_bordered(false);
  _search_box.add(&_search_text, true, true);
  scoped_connect(_search_text.signal_changed(), std::bind(&ConnectionsSection::on_search_text_changed, this));
  scoped_connect(_search_text.signal_action(),
                 std::bind(&ConnectionsSection::on_search_text_action, this, std::placeholders::_1));
  add(&_search_box, mforms::TopRight);

  set_padding(0, 30, CONNECTIONS_RIGHT_PADDING, 0);

  _add_button.title = "添加连接";
  _add_button.description = "添加连接";
  _add_button.defaultHandler = [this]() {
    _owner->trigger_callback(HomeScreenAction::ActionNewConnection, base::any());
  };

  _manage_button.title = "管理连接";
  _manage_button.description = "管理连接";
  _manage_button.defaultHandler = [this]() {
    _owner->trigger_callback(HomeScreenAction::ActionManageConnections, base::any());
  };

  _rescanButton.title = "重新扫描服务器";
  _rescanButton.description = "重新扫描服务器";
  _rescanButton.defaultHandler = [this]() {
    _owner->trigger_callback(HomeScreenAction::RescanLocalServers, base::any());
  };
}

//------------------------------------------------------------------------------------------------

ConnectionsSection::~ConnectionsSection() {
  if (_connection_context_menu != NULL)
    _connection_context_menu->release();
  if (_folder_context_menu != NULL)
    _folder_context_menu->release();
  if (_generic_context_menu != NULL)
    _generic_context_menu->release();

  deleteSurface(_folder_icon);
  deleteSurface(_network_icon);
  deleteSurface(_plus_icon);
  deleteSurface(_sakila_icon);
  deleteSurface(_user_icon);
  deleteSurface(_manage_icon);
}

//------------------------------------------------------------------------------------------------

void ConnectionsSection::updateColors() {
  if (_owner->isDarkModeActive()) {
    _titleColor = base::Color::parse("#F4F4F4");
    _folderTitleColor = base::Color::parse("#F0F0F0");
    _backgroundColor = base::Color::parse("#505050");
    _backgroundColorHot = base::Color::parse("#626160");
    _folderBackgroundColor = base::Color::parse("#3477a6");
    _folderBackgroundColorHot = base::Color::parse("#4699b8");
    _backTileBackgroundColor = base::Color::parse("#d9532c");
    _backTileBackgroundColorHot = base::Color::parse("#d97457");
  } else {
    _titleColor = base::Color::parse("#505050");
    _folderTitleColor = base::Color::parse("#F0F0F0");
    _backgroundColor = base::Color::parse("#F4F4F4");
    _backgroundColorHot = base::Color::parse("#D5D5D5");
    _folderBackgroundColor = base::Color::parse("#3477a6");
    _folderBackgroundColorHot = base::Color::parse("#4699b8");
    _backTileBackgroundColor = base::Color::parse("#d9532c");
    _backTileBackgroundColorHot = base::Color::parse("#d97457");

#ifndef __APPLE__
    _search_text.set_front_color("#000000");
    _search_text.set_placeholder_color("#303030");
    _search_text.set_back_color("#ffffff");
#endif
  }

  if (_welcomeScreen != nullptr)
    _welcomeScreen->updateColors();
}

//------------------------------------------------------------------------------------------------

void ConnectionsSection::updateIcons() {
  if (_owner->isDarkModeActive()) {
    deleteSurface(_sakila_icon);
    _sakila_icon = mforms::Utilities::load_icon("wb_tile_sakila_dark.png");

    deleteSurface(_manage_icon);
    _manage_icon = mforms::Utilities::load_icon("wb_tile_manage_dark.png");

    deleteSurface(_folder_icon);
    _folder_icon = mforms::Utilities::load_icon("wb_tile_folder.png");

    deleteSurface(_network_icon);
    _network_icon = mforms::Utilities::load_icon("wb_tile_network_dark.png");

    deleteSurface(_plus_icon);
    _plus_icon = mforms::Utilities::load_icon("wb_tile_plus_dark.png");

    deleteSurface(_user_icon);
    _user_icon = mforms::Utilities::load_icon("wb_tile_user_dark.png");
  } else {
    deleteSurface(_sakila_icon);
    _sakila_icon = mforms::Utilities::load_icon("wb_tile_sakila_light.png");

    deleteSurface(_manage_icon);
    _manage_icon = mforms::Utilities::load_icon("wb_tile_manage_light.png");

    deleteSurface(_folder_icon);
    _folder_icon = mforms::Utilities::load_icon("wb_tile_folder.png");

    deleteSurface(_network_icon);
    _network_icon = mforms::Utilities::load_icon("wb_tile_network_light.png");

    deleteSurface(_plus_icon);
    _plus_icon = mforms::Utilities::load_icon("wb_tile_plus_light.png");

    deleteSurface(_user_icon);
    _user_icon = mforms::Utilities::load_icon("wb_tile_user_light.png");
  }

  if (_welcomeScreen != nullptr)
    _welcomeScreen->updateIcons();
}

//------------------------------------------------------------------------------------------------

void ConnectionsSection::focus_search_box() {
  _search_text.focus();
}

//------------------------------------------------------------------------------------------------

void ConnectionsSection::showWelcomeHeading(bool state) {
  _showWelcomeHeading = state;
  if (_welcomeScreen != nullptr)
    _welcomeScreen->show(state);

  set_layout_dirty(true);
}

//------------------------------------------------------------------------------------------------

void ConnectionsSection::on_search_text_changed() {
  std::string filter = _search_text.get_string_value();
  _filtered_connections.clear();

  _filtered = !filter.empty();
  if (_filtered) {
    ConnectionVector current_connections = !_active_folder ? _connections : _active_folder->children;
    for (ConnectionIterator iterator = current_connections.begin(); iterator != current_connections.end(); ++iterator) {
      // Always keep the first entry if we are in a folder. It's not filtered.
      if (_active_folder && (iterator == current_connections.begin()))
        _filtered_connections.push_back(*iterator);
      else if (base::contains_string((*iterator)->search_title, filter, false) ||
               base::contains_string((*iterator)->search_description, filter, false) ||
               base::contains_string((*iterator)->search_user, filter, false) ||
               base::contains_string((*iterator)->search_schema, filter, false))
        _filtered_connections.push_back(*iterator);
    }
  }

  updateFocusableAreas();
  set_layout_dirty(true);
}

//------------------------------------------------------------------------------------------------

void ConnectionsSection::on_search_text_action(mforms::TextEntryAction action) {
  if (action == mforms::EntryEscape) {
    _search_text.set_value("");
    on_search_text_changed();
  } else if (action == mforms::EntryActivate) {
    if (_active_folder) {
      // Within a folder.
      switch (_filtered_connections.size()) {
        case 1: // Just the back tile. Return to top level.
          _active_folder.reset();
          _filtered = false;
          _search_text.set_value("");
          set_needs_repaint();
          break;

        case 2: // Exactly one entry matched the filter. Activate it.
          _owner->trigger_callback(HomeScreenAction::ActionOpenConnectionFromList,
                                   _filtered_connections[1]->connectionId);
          break;
      }
    } else {
      if (!_filtered_connections.empty()) {
        FolderEntry *folder = dynamic_cast<FolderEntry *>(_filtered_connections[0].get());
        // If only one entry is visible through filtering activate it. I.e. for a group show its content
        // and for a connection open it.
        if (folder && folder->children.size() > 1) {
          // Loop through the unfiltered list to find the index of the group we are about to open.
          _active_folder.reset(); // Just a defensive action. Should never play a role.
          for (size_t i = 0; i < _connections.size(); ++i) {
            if (_connections[i]->title == _filtered_connections[0]->title) {
              _active_folder = std::dynamic_pointer_cast<FolderEntry>(_connections[i]);
              break;
            }
          }
          _filtered = false;
          _search_text.set_value("");
          set_needs_repaint();
        } else
          _owner->trigger_callback(HomeScreenAction::ActionOpenConnectionFromList,
                                   _filtered_connections[0]->connectionId);
      }
    }
  }
}

//------------------------------------------------------------------------------------------------

/**
 * Computes the index for the given position, regardless if that is actually backed by an existing
 * entry or not.
 *
 * This will not work in section separated folders, but it doesn't matter
 * atm because this is only used for drag/drop
 */
ssize_t ConnectionsSection::calculate_index_from_point(int x, int y) {
  int width = get_width();
  if (x < CONNECTIONS_LEFT_PADDING || x > (width - CONNECTIONS_RIGHT_PADDING) || y < CONNECTIONS_TOP_PADDING)
    return -1; // Outside the tiles area.

  x -= CONNECTIONS_LEFT_PADDING;
  if ((x % (CONNECTIONS_TILE_WIDTH + CONNECTIONS_SPACING)) > CONNECTIONS_TILE_WIDTH)
    return -1; // Within the horizontal spacing between two tiles.

  y -= CONNECTIONS_TOP_PADDING;
  if ((y % (CONNECTIONS_TILE_HEIGHT + CONNECTIONS_SPACING)) > CONNECTIONS_TILE_HEIGHT)
    return -1; // Within the vertical spacing between two tiles.

  width -= CONNECTIONS_LEFT_PADDING + CONNECTIONS_RIGHT_PADDING;
  int tiles_per_row = width / (CONNECTIONS_TILE_WIDTH + CONNECTIONS_SPACING);
  if (x >= tiles_per_row * (CONNECTIONS_TILE_WIDTH + CONNECTIONS_SPACING))
    return -1; // After the last tile in a row.

  int height = get_height() - CONNECTIONS_TOP_PADDING;
  int column = x / (CONNECTIONS_TILE_WIDTH + CONNECTIONS_SPACING);
  int row = y / (CONNECTIONS_TILE_HEIGHT + CONNECTIONS_SPACING);

  int row_bottom = row * (CONNECTIONS_TILE_HEIGHT + CONNECTIONS_SPACING) + CONNECTIONS_TILE_HEIGHT;
  if (row_bottom > height)
    return -1; // The last visible row is dimmed if not fully visible. So take it out from hit tests too.

  return row * tiles_per_row + column;
}

//----------------------------------------------------------------------------------------------------------------------

std::shared_ptr<ConnectionEntry> ConnectionsSection::entry_from_point(int x, int y) const {
  std::shared_ptr<ConnectionEntry> entry;

  ConnectionVector connections(displayed_connections());
  for (ConnectionVector::iterator conn = connections.begin(); conn != connections.end(); ++conn) {
    if ((*conn)->bounds.contains(x, y)) {
      entry = *conn;
      break;
    }
  }

  return entry;
}

//----------------------------------------------------------------------------------------------------------------------

std::shared_ptr<ConnectionEntry> ConnectionsSection::entry_from_index(ssize_t index) const {
  ssize_t count = displayed_connections().size();
  if (index < count) {
    return displayed_connections()[index];
  }
  return std::shared_ptr<ConnectionEntry>();
}

//----------------------------------------------------------------------------------------------------------------------

base::Rect ConnectionsSection::bounds_for_entry(size_t index, size_t width) {
  base::Rect result(CONNECTIONS_LEFT_PADDING, CONNECTIONS_TOP_PADDING, CONNECTIONS_TILE_WIDTH, CONNECTIONS_TILE_HEIGHT);
  size_t tiles_per_row = (width - CONNECTIONS_LEFT_PADDING - CONNECTIONS_RIGHT_PADDING) /
                         (CONNECTIONS_TILE_WIDTH + CONNECTIONS_SPACING);

  if (tiles_per_row == 0)
    return result;

  size_t column = index % tiles_per_row;
  size_t row = index / tiles_per_row;
  result.pos.x += column * (CONNECTIONS_TILE_WIDTH + CONNECTIONS_SPACING);
  result.pos.y += row * (CONNECTIONS_TILE_HEIGHT + CONNECTIONS_SPACING);

  return result;
}

//----------------------------------------------------------------------------------------------------------------------

/**
 * Returns the connection id stored under the given index. Returns an empty string if the index
 * describes a folder or back tile.
 * Properly takes into account if we are in a folder or not and if we have filtered entries currently.
 */
std::string ConnectionsSection::connectionIdFromIndex(ssize_t index) {
  if (index < 0 || (_active_folder && index == 0))
    return "";

  return displayed_connections()[index]->connectionId;
}

//----------------------------------------------------------------------------------------------------------------------

void ConnectionsSection::repaint(cairo_t *cr, int areax, int areay, int areaw, int areah) {
  if (is_layout_dirty()) {
    getContainer()->get_parent()->relayout();
    set_layout_dirty(false);
  }

  int yoffset = 45;

  int width = get_width() - CONNECTIONS_LEFT_PADDING - CONNECTIONS_RIGHT_PADDING;

  int tiles_per_row = width / (CONNECTIONS_TILE_WIDTH + CONNECTIONS_SPACING);
  if (tiles_per_row < 1)
    tiles_per_row = 1;

  cairo_select_font_face(cr, mforms::HomeScreenSettings::HOME_TITLE_FONT, CAIRO_FONT_SLANT_NORMAL,
                         CAIRO_FONT_WEIGHT_BOLD);
  cairo_set_font_size(cr, mforms::HomeScreenSettings::HOME_TITLE_FONT_SIZE);

  cairo_set_source_rgba(cr, _titleColor.red, _titleColor.green, _titleColor.blue, _titleColor.alpha);
  cairo_move_to(cr, CONNECTIONS_LEFT_PADDING, yoffset);

  ConnectionVector const& connections(displayed_connections());
  std::string title = _("MySQL 连接");
  if (_active_folder)
    title += " / " + _active_folder->title;

  cairo_show_text(cr, title.c_str());

  // The + button after the title.
  cairo_text_extents_t extents;
  cairo_text_extents(cr, title.c_str(), &extents);
  double text_width = ceil(extents.width);

  _add_button.bounds = base::Rect(CONNECTIONS_LEFT_PADDING + text_width + 10, yoffset - imageHeight(_plus_icon) + 2,
                                  imageWidth(_plus_icon), imageHeight(_plus_icon));

  cairo_set_source_surface(cr, _plus_icon, _add_button.bounds.left(), _add_button.bounds.top());
  cairo_paint(cr);

  _manage_button.bounds = base::Rect(_add_button.bounds.right() + 4, yoffset - imageHeight(_manage_icon) + 2,
                                     imageWidth(_manage_icon), imageHeight(_manage_icon));
  cairo_set_source_surface(cr, _manage_icon, _manage_button.bounds.left(), _manage_button.bounds.top());
  cairo_paint(cr);

  int row = 0;

  base::Rect bounds(0, CONNECTIONS_TOP_PADDING, CONNECTIONS_TILE_WIDTH, CONNECTIONS_TILE_HEIGHT);

  if (connections.size() == 0) {
    std::string line1 = "MySQL Workbench 无法检测到任何正在运行的MySQL服务器。";
    std::string line2 = "这意味着MySQL没有安装或没有运行。";

    double x = get_width() / 2 - (extents.width / 2 + extents.x_bearing);
    int yoffset = static_cast<int>(bounds.top()) + 30;
    cairo_text_extents_t extents;
    cairo_set_source_rgb(cr, _titleColor.red, _titleColor.green, _titleColor.blue);
    cairo_set_font_size(cr, mforms::HomeScreenSettings::HOME_TITLE_FONT_SIZE * 0.8);
    cairo_text_extents(cr, line1.c_str(), &extents);

    x = get_width() / 2 - (extents.width / 2 + extents.x_bearing);
    yoffset += static_cast<int>(extents.height) + 10;

    cairo_move_to(cr, x, yoffset);
    cairo_show_text(cr, line1.c_str());

    cairo_text_extents(cr, line2.c_str(), &extents);

    x = get_width() / 2 - (extents.width / 2 + extents.x_bearing);
    yoffset += static_cast<int>(extents.height) + 10;

    cairo_move_to(cr, x, yoffset);
    cairo_show_text(cr, line2.c_str());

    cairo_select_font_face(cr, mforms::HomeScreenSettings::HOME_TITLE_FONT, CAIRO_FONT_SLANT_NORMAL,
                          CAIRO_FONT_WEIGHT_NORMAL);
    cairo_set_font_size(cr, mforms::HomeScreenSettings::HOME_TITLE_FONT_SIZE * 0.8);
    cairo_set_source_rgb(cr, 0x1b / 255.0, 0xad / 255.0, 0xe8 / 255.0);
    cairo_text_extents(cr, _rescanButton.title.c_str(), &extents);

    x = get_width() / 2 - (extents.width / 2 + extents.x_bearing);
    yoffset += static_cast<int>(extents.height) + 10;

    cairo_move_to(cr, x, yoffset);
    cairo_show_text(cr, _rescanButton.title.c_str());

    _rescanButton.bounds = base::Rect(x, yoffset - extents.height - 5, extents.width, extents.height + 10);

    return;
  }

  std::size_t index = 0;
  bool done = false;
  while (!done) {
    if (index >= connections.size())
      break; // we're done

    bounds.pos.x = CONNECTIONS_LEFT_PADDING;
    for (int column = 0; column < tiles_per_row; column++) {
      // Update the stored bounds of the tile.
      connections[index]->bounds = bounds;

      bool draw_hot = connections[index] == _hot_entry;
      connections[index]->draw_tile(cr, draw_hot, 1.0, false);

      // Draw drop indicator.
      if (static_cast<ssize_t>(index) == _drop_index) {
        if (mforms::App::get()->isDarkModeActive())
          cairo_set_source_rgb(cr, 1, 1, 1);
        else
          cairo_set_source_rgb(cr, 0, 0, 0);

        if (_drop_position == mforms::DropPositionOn) {
          double x = bounds.left() - 4;
          double y = bounds.ycenter();
          cairo_move_to(cr, x, y - 15);
          cairo_line_to(cr, x + 15, y);
          cairo_line_to(cr, x, y + 15);
          cairo_fill(cr);
        } else {
          double x = bounds.left() - 4.5;
          if (_drop_position == mforms::DropPositionRight)
            x = bounds.right() + 4.5;
          cairo_move_to(cr, x, bounds.top());
          cairo_line_to(cr, x, bounds.bottom());
          cairo_set_line_width(cr, 3);
          cairo_stroke(cr);
          cairo_set_line_width(cr, 1);
        }
      }

      index++;
      bounds.pos.x += CONNECTIONS_TILE_WIDTH + CONNECTIONS_SPACING;
      if (index >= connections.size()) {
        done = true; // we're done
        break;
      }
    }

    row++;
    bounds.pos.y += CONNECTIONS_TILE_HEIGHT + CONNECTIONS_SPACING;
  }

  mforms::DrawBox::repaint(cr, areax, areay, areaw, areah);
}

//----------------------------------------------------------------------------------------------------------------------

base::Size ConnectionsSection::getLayoutSize(base::Size proposedSize) {
  ConnectionVector const& connections(displayed_connections());

  size_t height;
  if (connections.empty())
    height = CONNECTIONS_TOP_PADDING + CONNECTIONS_BOTTOM_PADDING;
  else {
    base::Rect bounds = bounds_for_entry(connections.size() - 1, (size_t)proposedSize.width);
    height = (size_t)bounds.bottom() + CONNECTIONS_BOTTOM_PADDING;
  }

  return base::Size(proposedSize.width, (double)height);
}

//----------------------------------------------------------------------------------------------------------------------

const char* ConnectionsSection::getTitle() {
  return "Connections Section";
}

//----------------------------------------------------------------------------------------------------------------------

void ConnectionsSection::cancelOperation() {
  // noop
}

//----------------------------------------------------------------------------------------------------------------------

void ConnectionsSection::setFocus() {
  _search_text.focus();
}

//----------------------------------------------------------------------------------------------------------------------

bool ConnectionsSection::canHandle(HomeScreenMenuType type) {
  switch (type) {
    case HomeMenuConnection:
    case HomeMenuConnectionGroup:
    case HomeMenuConnectionGeneric:
      return true;
    default:
      return false;
  }
  return false;
}

//------------------------------------------------------------------------------------------------

void ConnectionsSection::setContextMenu(mforms::Menu *menu, HomeScreenMenuType type) {
  if (canHandle(type)) {
    switch (type) {
      case HomeMenuConnectionGroup:
        if (_folder_context_menu != NULL)
          _folder_context_menu->release();
        _folder_context_menu = menu;
        if (_folder_context_menu != NULL) {
          _folder_context_menu->retain();
          menu->set_handler(std::bind(&ConnectionsSection::handle_folder_command, this, std::placeholders::_1));
        }
        break;

      case HomeMenuConnection:
        if (_connection_context_menu != NULL)
          _connection_context_menu->release();
        _connection_context_menu = menu;
        if (_connection_context_menu != NULL) {
          _connection_context_menu->retain();
          menu->set_handler(std::bind(&ConnectionsSection::handle_command, this, std::placeholders::_1));
        }
        break;

      default:
        if (_generic_context_menu != NULL)
          _generic_context_menu->release();
        _generic_context_menu = menu;
        if (_generic_context_menu != NULL) {
          _generic_context_menu->retain();
          menu->set_handler(std::bind(&ConnectionsSection::handle_command, this, std::placeholders::_1));
        }
        break;
    }

    if (menu != NULL)
      scoped_connect(menu->signal_will_show(), std::bind(&ConnectionsSection::menu_open, this));
  }
}

//------------------------------------------------------------------------------------------------

void ConnectionsSection::setContextMenuAction(mforms::Menu *menu, HomeScreenMenuType type) {
  // pass
}

//------------------------------------------------------------------------------------------------

void ConnectionsSection::addConnection(const std::string &connectionId, const std::string &title,
                                       const std::string &description, const std::string &user,
                                       const std::string &schema) {
  std::shared_ptr<ConnectionEntry> entry;

  entry = std::shared_ptr<ConnectionEntry>(new ConnectionEntry(this));

  entry->connectionId = connectionId;
  entry->title = title;
  entry->description = description;
  entry->user = user;
  entry->schema = schema;
  entry->compute_strings = true;

  entry->search_title = title;
  entry->search_description = description;
  entry->search_user = user;
  entry->search_schema = schema;

  std::string::size_type slash_position = title.find("/");
  if (slash_position != std::string::npos) {
    // A child entry->
    std::string parent_name = title.substr(0, slash_position);
    entry->title = title.substr(slash_position + 1);
    entry->search_title = entry->title;
    bool found_parent = false;
    for (ConnectionIterator iterator = _connections.begin(); iterator != _connections.end(); iterator++) {
      if ((*iterator)->title == parent_name) {
        if (FolderEntry *folder = dynamic_cast<FolderEntry *>(iterator->get())) {
          found_parent = true;
          folder->children.push_back(entry);
          break;
        }
      }
    }

    // If the parent was not found, a folder should be created
    if (!found_parent) {
      std::shared_ptr<FolderEntry> folder(new FolderEntry(this));

      folder->description = parent_name;
      folder->title = parent_name;
      folder->compute_strings = true;
      folder->search_title = parent_name;

      folder->children.push_back(std::shared_ptr<ConnectionEntry>(new FolderBackEntry(this)));
      folder->children.push_back(entry);
      _connections.push_back(std::dynamic_pointer_cast<ConnectionEntry>(folder));
      if (!_active_folder_title_before_refresh_start.empty() &&
          _active_folder_title_before_refresh_start == folder->title) {
        _active_folder = std::dynamic_pointer_cast<FolderEntry>(_connections.back());
        _active_folder_title_before_refresh_start.clear();
      }
    }
  } else
    _connections.push_back(entry);

  set_layout_dirty(true);
}

void ConnectionsSection::updateFocusableAreas() {
  clearFocusableAreas();
  if (!_filtered_connections.empty()) {
    for (const auto &it: _filtered_connections) {
      mforms::FocusableArea fArea;
      fArea.activate = [it]() { it->activate(); };
      fArea.getBounds = [it]() { return it->getAccessibilityBounds(); };
      fArea.showContextMenu = [it, this]() {
        const auto bounds = it->getAccessibilityBounds();
        // We need to set the _entry_for_menu otherwise,
        // some options may crash the app.
        _entry_for_menu = entry_from_point(static_cast<int>(bounds.center().x), static_cast<int>(bounds.center().y));
        it->context_menu()->popup_at(this, static_cast<int>(bounds.center().x), static_cast<int>(bounds.center().y));
      };
      addFocusableArea(fArea);
    }
  } else {
    ConnectionVector current_connections = !_active_folder ? _connections : _active_folder->children;
    for (const auto &it: current_connections) {
      mforms::FocusableArea fArea;
      fArea.activate = [it]() { it->activate(); };
      fArea.getBounds = [it]() { return it->getAccessibilityBounds(); };
      fArea.showContextMenu = [it, this]() {
        const auto bounds = it->getAccessibilityBounds();
        // We need to set the _entry_for_menu otherwise,
        // some options may crash the app.
        _entry_for_menu = entry_from_point(static_cast<int>(bounds.center().x), static_cast<int>(bounds.center().y));
        it->context_menu()->popup_at(this, static_cast<int>(bounds.center().x), static_cast<int>(bounds.center().y));
      };
      addFocusableArea(fArea);
    }
  }
}

//------------------------------------------------------------------------------------------------

bool ConnectionsSection::setFocusOnEntry(ConnectionEntry const* entry) {
  return setFocusOnArea(entry->bounds.center());
}

//------------------------------------------------------------------------------------------------

void ConnectionsSection::clear_connections(bool clear_state) {
  if (clear_state) {
    _filtered = false;
    _filtered_connections.clear();
    _search_text.set_value("");
    _active_folder_title_before_refresh_start = "";
  } else {
    if (_active_folder)
      _active_folder_title_before_refresh_start = _active_folder->title;
  }
  clearFocusableAreas();
  _entry_for_menu.reset();
  _active_folder.reset();
  _connections.clear();

  set_layout_dirty(true);
}

//------------------------------------------------------------------------------------------------

void ConnectionsSection::change_to_folder(std::shared_ptr<FolderEntry> folder) {
  if (_active_folder && !folder) {
    // Returning to root list.
    _active_folder.reset();
    _filtered = false;
    _search_text.set_value("");
    updateFocusableAreas();
    set_layout_dirty(true);
  } else if (folder) {
    _active_folder = folder;
    // Drilling into a folder.
    _filtered = false;
    _search_text.set_value("");
    updateFocusableAreas();
    set_layout_dirty(true);
  }
}

//--------------------------------------------------------------------------------------------------

bool ConnectionsSection::mouse_down(mforms::MouseButton button, int x, int y) {
  mforms::DrawBox::mouse_down(button, x, y);
  if (button == mforms::MouseButtonLeft && _hot_entry)
    _mouse_down_position = base::Rect(x - 4, y - 4, 8, 8); // Center a 8x8 pixels rect around the mouse position.

  return false;                                            // Continue with standard mouse handling.
}

//--------------------------------------------------------------------------------------------------

bool ConnectionsSection::mouse_up(mforms::MouseButton button, int x, int y) {
  _mouse_down_position = base::Rect();
  return false;
}

//--------------------------------------------------------------------------------------------------

bool ConnectionsSection::mouse_double_click(mforms::MouseButton button, int x, int y) {
  return false;
}

//--------------------------------------------------------------------------------------------------

bool ConnectionsSection::mouse_click(mforms::MouseButton button, int x, int y) {
  // everything below this relies on _hot_entry, which will become out of sync
  // if the user pops up the context menu and then clicks (or right clicks) in some
  // other tile... so we must first update _hot_entry before doing any actions
  mouse_move(mforms::MouseButtonNone, x, y);

  switch (button) {
    case mforms::MouseButtonLeft: {
      if (_add_button.bounds.contains(x, y)) {
        _owner->trigger_callback(HomeScreenAction::ActionNewConnection, base::any());
        return true;
      }

      if (_manage_button.bounds.contains(x, y)) {
        _owner->trigger_callback(HomeScreenAction::ActionManageConnections, base::any());
        return true;
      }

      if (_rescanButton.bounds.contains(x, y)) {
        _owner->trigger_callback(HomeScreenAction::RescanLocalServers, base::any());
        return true;
      }

      if (_hot_entry) {
        _hot_entry->activate();
        return true;
      }

      break;
    }

    case mforms::MouseButtonRight: {
      mforms::Menu *context_menu = NULL;

      if (_hot_entry) {
        context_menu = _hot_entry->context_menu();

        _entry_for_menu = _hot_entry;
      } else
        context_menu = _generic_context_menu;

      // At this point the context menu and the associated entry have been selected
      if (context_menu)
        context_menu->popup_at(this, x, y);

      break;
    }

    default:
      break;
  }

  return false;
}

//------------------------------------------------------------------------------------------------

bool ConnectionsSection::mouse_leave() {
  if (_hot_entry) {
    _hot_entry.reset();
    set_needs_repaint();
  }
  return false;
}

//------------------------------------------------------------------------------------------------

bool ConnectionsSection::mouse_move(mforms::MouseButton button, int x, int y) {

  std::shared_ptr<ConnectionEntry> entry = entry_from_point(x, y);

  if (entry && !_mouse_down_position.empty() && (!_mouse_down_position.contains(x, y))) {
    if (!entry->is_movable()) {
      _mouse_down_position = base::Rect();
      return true;
    }

    if (button == mforms::MouseButtonNone) // Cancel drag if the mouse button was released.
      return true;

    return do_tile_drag(calculate_index_from_point(x, y), x, y);
  } else {
    // Only do hit tracking if no mouse button is pressed to avoid situations like
    // mouse down outside any tile, drag over a tile, release mouse button -> click
    // (or hover effects in general).
    if (button == mforms::MouseButtonNone) {
      if (entry != _hot_entry) {
        _hot_entry = entry;
        set_needs_repaint();
        return true;
      }
    }
  }

  return false;
}

//------------------------------------------------------------------------------------------------

void ConnectionsSection::handle_command(const std::string &command) {
  std::string item;
  if (_entry_for_menu) {
    if (_active_folder) {
      if (command == "delete_connection_all") {
        // We only want to delete all connections in the active group. This is the same as
        // removing the group entirely, since the group is formed by connections in it.
        _entry_for_menu = _active_folder;
        handle_folder_command("delete_connection_group");
        return;
      } else {
        item = _entry_for_menu->connectionId;
      }
    } else {
      item = _entry_for_menu->connectionId;
    }
  }

  _owner->handleContextMenu(item, command);
  _entry_for_menu.reset();
}

//------------------------------------------------------------------------------------------------

void ConnectionsSection::handle_folder_command(const std::string &command) {
  {
    // We have to pass on a valid connection (for the group name).
    // All child items have the same group name (except the dummy entry for the back tile).
    std::string title;
    if (_entry_for_menu)
      title = _entry_for_menu->title;

    title += "/";

    _owner->handleContextMenu(title, command);
    _entry_for_menu.reset();
  }
}

//------------------------------------------------------------------------------------------------

void ConnectionsSection::menu_open() {
  if (_entry_for_menu) {
    ConnectionVector const& items(displayed_connections());

    if (items.empty())
      _entry_for_menu->menu_open(ConnectionEntry::Other);
    else if (items.front() == _entry_for_menu)
      _entry_for_menu->menu_open(ConnectionEntry::First);
    else if (items.back() == _entry_for_menu)
      _entry_for_menu->menu_open(ConnectionEntry::Last);
    else
      _entry_for_menu->menu_open(ConnectionEntry::Other);
  }
}

//------------------------------------------------------------------------------------------------

std::string ConnectionsSection::getAccessibilityTitle() {
  return "Home Screen Connections List";
}

//------------------------------------------------------------------------------------------------

size_t ConnectionsSection::getAccessibilityChildCount() {
  size_t ret_val = 2; // 2 for create + manage icons.

  if (_filtered)
    ret_val += _filtered_connections.size();
  else if (!_active_folder)
    ret_val += _connections.size();
  else
    ret_val += _active_folder->children.size();

  return ret_val;
}

//------------------------------------------------------------------------------------------------

base::Accessible* ConnectionsSection::getAccessibilityChild(size_t index) {
  base::Accessible* accessible = nullptr;

  switch (index) {
    case 0:
      accessible = &_add_button;
      break;
    case 1:
      accessible = &_manage_button;
      break;
    default: {
      index -= 2; // Offset the index for the buttons handled above.

      if (_filtered) {
        if (index < _filtered_connections.size())
          accessible = _filtered_connections[index].get();
      } else {
        if (!_active_folder) {
          if (index < _connections.size())
            accessible = _connections[index].get();
        } else {
          if (index < _active_folder->children.size())
            accessible = _active_folder->children[index].get();
        }
      }
    }
  }

  return accessible;
}

//------------------------------------------------------------------------------------------------

base::Accessible::Role ConnectionsSection::getAccessibilityRole() {
  return Accessible::List;
}

//------------------------------------------------------------------------------------------------

base::Accessible* ConnectionsSection::accessibilityHitTest(ssize_t x, ssize_t y) {
  base::Accessible* accessible = nullptr;

  if (_add_button.bounds.contains(static_cast<double>(x), static_cast<double>(y)))
    accessible = &_add_button;
  else if (_manage_button.bounds.contains(static_cast<double>(x), static_cast<double>(y)))
    accessible = &_manage_button;
  else {
    std::shared_ptr<ConnectionEntry> entry = entry_from_point(static_cast<int>(x), static_cast<int>(y));

    if (entry)
      accessible = entry.get();
  }

  return accessible;
}

//------------------------------------------------------------------------------------------------

bool ConnectionsSection::do_tile_drag(ssize_t index, int x, int y) {
  _hot_entry.reset();
  set_needs_repaint();

  if (index >= 0) {
    mforms::DragDetails details;
    details.allowedOperations = mforms::DragOperationMove;
    details.location = base::Point(x, y);

    details.image = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, CONNECTIONS_TILE_WIDTH, CONNECTIONS_TILE_HEIGHT);
    cairo_t *cr = cairo_create(details.image);
    base::Rect bounds = bounds_for_entry(index, get_width());
    details.hotspot.x = x - bounds.pos.x;
    details.hotspot.y = y - bounds.pos.y;

    // We know we have no back tile here.
    std::shared_ptr<ConnectionEntry> entry = entry_from_index(index);
    if (entry) {
      entry->draw_tile(cr, false, 1, true);

      _drag_index = index;
      mforms::DragOperation operation =
        do_drag_drop(details, entry.get(), mforms::HomeScreenSettings::TILE_DRAG_FORMAT);

      _mouse_down_position = base::Rect();
      cairo_surface_destroy(details.image);
      cairo_destroy(cr);

      _drag_index = -1;
      _drop_index = -1;
      set_layout_dirty(true);

      if (operation == mforms::DragOperationMove) // The actual move is done in the drop delegate method.
        return true;
    }
  }
  return false;
}

//------------------------------------------------------------------------------------------------

// Drop delegate implementation.
mforms::DragOperation ConnectionsSection::drag_over(View *sender, base::Point p,
                                                    mforms::DragOperation allowedOperations,
                                                    const std::vector<std::string> &formats) {
  if (allowedOperations == mforms::DragOperationNone)
    return allowedOperations;

  if (std::find(formats.begin(), formats.end(), mforms::DragFormatFileName) != formats.end()) {
    // Indicate we can accept files if one of the connection tiles is hit.
    std::shared_ptr<ConnectionEntry> entry = entry_from_point((int)p.x, (int)p.y);

    if (!entry)
      return mforms::DragOperationNone;

    if (entry->connectionId.empty())
      return mforms::DragOperationNone;

    if (_hot_entry != entry) {
      _hot_entry = entry;
      set_needs_repaint();
    }
    return allowedOperations & mforms::DragOperationCopy;
  }

  if (std::find(formats.begin(), formats.end(), mforms::HomeScreenSettings::TILE_DRAG_FORMAT) != formats.end()) {
    // A tile is being dragged. Find the target index and drop location for visual feedback.
    // Computation here is more relaxed than the normal hit test as we want to allow dropping
    // left, right and below the actual tiles area too as well as between tiles.
    if (p.y < CONNECTIONS_TOP_PADDING) {
      if (_drop_index > -1) {
        _drop_index = -1;
        set_needs_repaint();
      }
      return mforms::DragOperationNone;
    }

    p.y -= CONNECTIONS_TOP_PADDING;

    int count = (int)_connections.size();
    if (_filtered)
      count = (int)_filtered_connections.size(); // For both, main list or folder.
    else if (_active_folder)
      count = (int)_active_folder->children.size();

    int width = get_width();
    int tiles_per_row =
      (width - CONNECTIONS_LEFT_PADDING - CONNECTIONS_RIGHT_PADDING) / (CONNECTIONS_TILE_WIDTH + CONNECTIONS_SPACING);

    int column = -1; // Left to the first column.
    if (p.x > (width - CONNECTIONS_RIGHT_PADDING))
      column = tiles_per_row; // After last column.
    else if (p.x >= CONNECTIONS_LEFT_PADDING)
      column = (int)((p.x - CONNECTIONS_LEFT_PADDING) / (CONNECTIONS_TILE_WIDTH + CONNECTIONS_SPACING));

    int row = (int)(p.y / (CONNECTIONS_TILE_HEIGHT + CONNECTIONS_SPACING));

    int row_bottom = row * (CONNECTIONS_TILE_HEIGHT + CONNECTIONS_SPACING) + CONNECTIONS_TILE_HEIGHT;
    if (row_bottom > get_height()) {
      if (_drop_index > -1) {
        _drop_index = -1;
        set_needs_repaint();
      }
      return mforms::DragOperationNone; // Drop on the dimmed row. No drop action here.
    }

    int index = (int)(row * tiles_per_row);
    if (column == tiles_per_row)
      index += column - 1;
    else if (column > -1)
      index += column;

    mforms::DropPosition position = mforms::DropPositionLeft;
    if (column == tiles_per_row)
      position = mforms::DropPositionRight;
    else {
      if (index >= count) {
        index = count - 1;
        position = mforms::DropPositionRight;
      } else {
        // Tile hit. Depending on which side of the tile's center the mouse is use a position
        // before or after that tile. Back tiles have no "before" position, but only "on" or "after".
        // Folder tiles have "before", "on" and "after" positions. Connection tiles only have "before"
        // and "after".
        base::Rect bounds = bounds_for_entry(index, get_width());
        std::shared_ptr<ConnectionEntry> entry = entry_from_index(index);
        if (entry && dynamic_cast<FolderEntry *>(entry.get())) {
          // In a group take the first third as hit area for "before", the second as "on" and the
          // last one as "after".
          if (p.x > bounds.left() + bounds.width() / 3) {
            if (p.x > bounds.right() - bounds.width() / 3)
              position = mforms::DropPositionRight;
            else
              position = mforms::DropPositionOn;
          }
        } else {
          if (p.x > bounds.xcenter())
            position = mforms::DropPositionRight;
        }
      }
    }

    // Check that the drop position does not resolve to the dragged item.
    // Don't allow dragging a group on a group either.
    if (_drag_index > -1 &&
        (index == _drag_index || (index + 1 == _drag_index && position == mforms::DropPositionRight) ||
         (index - 1 == _drag_index && position == mforms::DropPositionLeft) ||
         (position == mforms::DropPositionOn && dynamic_cast<FolderEntry *>(entry_from_index(_drag_index).get())))) {
      index = -1;
    } else if (!_filtered && _active_folder && index == 0 && position == mforms::DropPositionLeft) {
      position = mforms::DropPositionOn; // Drop on back tile.
    }

    if (_drop_index != index || _drop_position != position) {
      _drop_index = index;
      _drop_position = position;
      set_needs_repaint();
    }

    return mforms::DragOperationMove;
  }

  return mforms::DragOperationNone;
}

//----------------------------------------------------------------------------------------------------------------------

mforms::DragOperation ConnectionsSection::files_dropped(View *sender, base::Point p,
                                                        mforms::DragOperation allowedOperations,
                                                        const std::vector<std::string> &file_names) {
  std::shared_ptr<ConnectionEntry> entry = entry_from_point(static_cast<int>(p.x), static_cast<int>(p.y));
  if (!entry)
    return mforms::DragOperationNone;

  if (!entry->connectionId.empty()) {
    // Allow only sql script files to be dropped.
    std::vector<std::string> files;
    for (size_t i = 0; i < file_names.size(); ++i)
      if (base::tolower(base::extension(file_names[i])) == ".sql")
        files.push_back(file_names[i]);

    if (files.size() == 0)
      return mforms::DragOperationNone;

    HomeScreenDropFilesInfo dInfo;
    dInfo.connectionId = entry->connectionId;
    dInfo.files = files;
    _owner->trigger_callback(HomeScreenAction::ActionFilesWithConnection, dInfo);
  }

  return mforms::DragOperationCopy;
}

//----------------------------------------------------------------------------------------------------------------------

mforms::DragOperation ConnectionsSection::data_dropped(mforms::View *sender, base::Point p,
                                                       mforms::DragOperation allowedOperations, void *data,
                                                       const std::string &format) {
  if (format == mforms::HomeScreenSettings::TILE_DRAG_FORMAT && _drop_index > -1) {
    mforms::DragOperation result = mforms::DragOperationNone;

    // Can be invalid if we move a group.
    std::string connectionId = connectionIdFromIndex(_drag_index);
    ConnectionEntry *source_entry = static_cast<ConnectionEntry *>(data);

    std::shared_ptr<ConnectionEntry> entry;
    if (_filtered) {
      if (_drop_index < (int)_filtered_connections.size())
        entry = _filtered_connections[_drop_index];
    } else if (_active_folder) {
      if (_drop_index < (int)_active_folder->children.size())
        entry = _active_folder->children[_drop_index];
    } else {
      if (_drop_index < (int)_connections.size())
        entry = _connections[_drop_index];
    }

    if (!entry)
      return result;

    bool is_back_tile = entry->title == "< back";

    // Drop target is a group.
    HomeScreenDropInfo dropInfo;
    if (!connectionId.empty()) {
      dropInfo.valueIsConnectionId = true;
      dropInfo.value = connectionId;
    } else
      dropInfo.value = source_entry->title + "/";

    if (_drop_position == mforms::DropPositionOn) {
      // Drop on a group (or back tile).
      if (is_back_tile)
        dropInfo.group = "*Ungrouped*";
      else
        dropInfo.group = entry->title;
      _owner->trigger_callback(HomeScreenAction::ActionMoveConnectionToGroup, dropInfo);
    } else {
      // Drag from one position to another within a group (root or active group).
      size_t to = _drop_index;
      if (_active_folder)
        to--; // The back tile has no representation in the global list.
      if (_drop_position == mforms::DropPositionRight)
        to++;
      dropInfo.to = to;
      _owner->trigger_callback(HomeScreenAction::ActionMoveConnection, dropInfo);
    }
    result = mforms::DragOperationMove;

    _drop_index = -1;
    set_layout_dirty(true);

    return result;
  }
  return mforms::DragOperationNone;
}

//----------------------------------------------------------------------------------------------------------------------

mforms::View *ConnectionsSection::getContainer() {
  if (_container == nullptr) {
    _container = mforms::manage(new mforms::Box(false));
    _container->set_name("Home Screen Content Host");
    _welcomeScreen = mforms::manage(new ConnectionsWelcomeScreen(_owner));
    if (!_showWelcomeHeading)
      _welcomeScreen->show(false);
    _welcomeScreen->set_name("Home Screen Welcome Page");
    _welcomeScreen->setInternalName("welcomeScreen");
    _welcomeScreen->set_layout_dirty(true);
    _container->add(_welcomeScreen, false, true);
    _container->add(this, true, true);
  }
  return _container;
}

//----------------------------------------------------------------------------------------------------------------------

mforms::View *ConnectionsSection::get_parent() const {
  return _container->get_parent();
}

//----------------------------------------------------------------------------------------------------------------------

ConnectionsSection::ConnectionVector const& ConnectionsSection::displayed_connections() const {
  if (_filtered)
    return _filtered_connections;
  else if (_active_folder)
    return _active_folder->children;
  else
    return _connections;
}

//----------------------------------------------------------------------------------------------------------------------
